﻿// ***********************************************************************
// Assembly         : Tx.OCC
// Author           : tianteng
// Created          : 06-26-2019
//
// Last Modified By : tianteng
// Last Modified On : 06-26-2019
// ***********************************************************************
// <copyright file="OccFaceEx.cs" company="北京腾雪科技有限责任公司">
//     Copyright © 北京腾雪科技有限责任公司 2022
// </copyright>
// <summary></summary>
// ***********************************************************************
using Occt;
using System.Collections.Generic;
using System.Linq;
using System;

namespace Tx.OCC
{
    /// <summary>
    /// Class OccFaceEx.
    /// </summary>
    public static class OccFaceEx
    {
        /// <summary>
        /// Gets the normal.
        /// </summary>
        /// <param name="face">The face.</param>
        /// <returns>gp_Vec.</returns>
        public static gp_Vec GetNormal(this TopoDS_Face face)
        {
            var surface = new BRepAdaptor_Surface(face);
            var u1 = surface.FirstUParameter;
            var u2 = surface.LastUParameter;
            var v1 = surface.FirstVParameter;
            var v2 = surface.LastVParameter;

            surface.D1((u1 + u2) / 2, (v1 + v2) / 2, out var aCenterOfFace, out var aVec1, out var aVec2);

            var theNormal = aVec1 ^ aVec2;
            theNormal.Normalize();

            // 田腾 2015/3/13 16:25:09 这里用来调整法向量，修改后有可能是错的
            if (face.NormalNeedReverse())
                theNormal.Reverse();

            return theNormal;
        }

        /// <summary>
        /// Normals the need reverse.
        /// </summary>
        /// <param name="face">The face.</param>
        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
        public static bool NormalNeedReverse(this TopoDS_Face face)
        {
            var orientation = face.Orientation;
            return orientation == TopAbs_Orientation.TopAbs_REVERSED || orientation == TopAbs_Orientation.TopAbs_INTERNAL;
        }

        /// <summary>
        /// Gets the type of the surface.
        /// </summary>
        /// <param name="face">a face.</param>
        /// <returns>SurfaceType.</returns>
        public static GeomAbs_SurfaceType GetSurfaceType(this TopoDS_Face face)
        {
            return new BRepAdaptor_Surface(face).Type;
        }

        /// <summary>
        /// Gets the plane from face.
        /// </summary>
        /// <param name="face">The face.</param>
        /// <param name="plane">The plane.</param>
        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
        public static bool GetPlaneFromFace(TopoDS_Face face, out gp_Pln plane)
        {
            var surfaceAdaptor = new BRepAdaptor_Surface(face);
            if (surfaceAdaptor.Type == GeomAbs_SurfaceType.GeomAbs_Plane)
            {
                plane = null;
                return false;
            }
            plane = surfaceAdaptor.Plane;
            return true;
        }

        /// <summary>
        /// Gets the centered plane from face.
        /// </summary>
        /// <param name="shape">The shape.</param>
        /// <param name="faceIndex">Index of the face.</param>
        /// <param name="plane">The plane.</param>
        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
        public static bool GetCenteredPlaneFromFace(TopoDS_Face occShape, int faceIndex, out gp_Pln plane, Action<string> messageError)
        {
            if (occShape == null)
            {
                messageError?.Invoke("Shape cannot be made, face parameters not available.");
                plane = gp_Pln.XOY;
                return false;
            }

            var faces = occShape.Faces(true);
            if (faces.Count <= faceIndex)
            {
                messageError?.Invoke("Shape has less faces than required face index.");
                plane = gp_Pln.XOY;
                return false;
            }

            return GetCenteredPlaneFromFace(faces[faceIndex], out plane, messageError);
        }

        /// <summary>
        /// Gets the centered plane from face.
        /// </summary>
        /// <param name="face">The face.</param>
        /// <param name="plane">The plane.</param>
        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
        public static bool GetCenteredPlaneFromFace(TopoDS_Face face, out gp_Pln plane, Action<string> messageError)
        {
            var brepAdaptor = new BRepAdaptor_Surface(face, true);
            if (brepAdaptor.Type != GeomAbs_SurfaceType.GeomAbs_Plane)
            {
                messageError?.Invoke("Selected face is not a plane type surface.");
                plane = gp_Pln.XOY;
                return false;
            }

            double centerU = brepAdaptor.FirstUParameter + (brepAdaptor.LastUParameter - brepAdaptor.FirstUParameter) / 2;
            double centerV = brepAdaptor.FirstVParameter + (brepAdaptor.LastVParameter - brepAdaptor.FirstVParameter) / 2;
            var centerPnt = brepAdaptor.Value(centerU, centerV);

            var dir = brepAdaptor.Plane.Position.Direction;
            if (face.Orientation == TopAbs_Orientation.TopAbs_REVERSED)
                dir.Reverse();

            plane = new gp_Pln(centerPnt, dir);
            return true;
        }

        /// <summary>
        /// Finds the connected faces.
        /// </summary>
        /// <param name="shape">The shape.</param>
        /// <param name="face">The face.</param>
        /// <returns>Dictionary&lt;TopoDS_Face, TopoDS_Edge&gt;.</returns>
        public static Dictionary<TopoDS_Face, TopoDS_Edge> FindConnectedFaces(TopoDS_Shape shape, TopoDS_Face face)
        {
            var list = new Dictionary<TopoDS_Face, TopoDS_Edge>();

            // Get map of all faces with their ancestors
            TopExp.MapShapesAndAncestors(shape, TopAbs_ShapeEnum.TopAbs_EDGE, TopAbs_ShapeEnum.TopAbs_FACE, out TopTools_IndexedDataMapOfShapeListOfShape faceMap);

            foreach (var edge in face.Edges())
            {
                var faceList = faceMap.FindFromKey(edge).ToList();
                foreach (var connectedFace in faceList)
                {
                    if (connectedFace.IsSame(face))
                        continue;

                    if (list.Keys.Any(f => connectedFace.IsSame(f)))
                        continue;

                    list.Add(connectedFace.ToFace(), edge);
                }
            }

            return list;
        }

        /// <summary>
        /// Finds the connected face.
        /// </summary>
        /// <param name="shape">The shape.</param>
        /// <param name="face">The face.</param>
        /// <param name="sharedEdge">The shared edge.</param>
        /// <returns>TopoDS_Face.</returns>
        public static TopoDS_Face FindConnectedFace(TopoDS_Shape shape, TopoDS_Face face, TopoDS_Edge sharedEdge)
        {
            // Get map of all faces with their ancestors
            TopExp.MapShapesAndAncestors(shape, TopAbs_ShapeEnum.TopAbs_EDGE, TopAbs_ShapeEnum.TopAbs_FACE, out TopTools_IndexedDataMapOfShapeListOfShape faceMap);

            foreach (var edge in face.Edges())
            {
                if (edge.IsSame(sharedEdge))
                {
                    var faceList = faceMap.FindFromKey(edge).ToList();
                    foreach (var connectedFace in faceList)
                    {
                        if (connectedFace.IsSame(face))
                            continue;

                        return connectedFace.ToFace();
                    }
                }
            }
            return null;
        }

        /// <summary>
        /// Gets the plane of faces.
        /// </summary>
        /// <param name="shape">The shape.</param>
        /// <param name="plane">The plane.</param>
        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
        public static bool GetPlaneOfFaces(TopoDS_Shape shape, out gp_Pln plane, Action<string> messageError)
        {
            plane = gp_Pln.XOY;
            var faces = shape.Faces();
            if (faces.Count == 0)
            {
                messageError?.Invoke("Cannot get plane of faces, shape doesn't have faces.");
                return false;
            }

            bool havePlane = false;
            foreach (var face in faces)
            {
                var brepAdaptor = new BRepAdaptor_Surface(face, true);
                if (brepAdaptor.Type != GeomAbs_SurfaceType.GeomAbs_Plane)
                {
                    messageError?.Invoke("Cannot get plane of faces, shape has faces which are not plane.");
                    return false;
                }

                if (havePlane)
                {
                    if (!plane.Position.IsCoplanar(brepAdaptor.Plane.Position, 0.00001, 0.00001))
                    {
                        messageError?.Invoke("Cannot get plane of faces, not all faces are coplanar.");
                        return false;
                    }
                }
                else
                {
                    plane = brepAdaptor.Plane;
                    havePlane = true;
                }
            }

            return true;
        }

        /// <summary>
        /// Gets the face center normal.
        /// </summary>
        /// <param name="face">The face.</param>
        /// <returns>gp_Ax1.</returns>
        public static gp_Ax1 GetFaceCenterNormal(TopoDS_Face face)
        {
            double umin = 0, umax = 0, vmin = 0, vmax = 0;
            BRepTools.UVBounds(face, out umin, out umax, out vmin, out vmax);

            var props = new BRepGProp_Face(face);
            gp_Pnt position = new gp_Pnt();
            gp_Vec normal = new gp_Vec();
            props.Normal((umin + umax) / 2.0, (vmin + vmax) / 2.0, out position, out normal);

            return new gp_Ax1(position, new gp_Dir(normal));
        }

        /// <summary>
        /// Searches for the face of a shape, and find the parallel face, both by area comparison
        /// </summary>
        /// <param name="shape">The shape.</param>
        /// <param name="comparer">The comparer.</param>
        /// <returns>System.ValueTuple&lt;TopoDS_Face, System.Nullable&lt;gp_Pln&gt;, TopoDS_Face, System.Nullable&lt;gp_Pln&gt;&gt;.</returns>
//         public static (TopoDS_Face face, gp_Pln plane, TopoDS_Face opFace, gp_Pln opPlane) FindFaceByAreaSize(TopoDS_Shape shape, Func<double, double, bool> comparer)
//         {
//             TopoDS_Face foundFace = null;
//             gp_Pln foundPlane = null;
//             TopoDS_Face opFace = null;
//             gp_Pln opPlane = null;
// 
//             // Find the longest face
//             double faceArea = double.NaN;
//             var faceInfos = new List<(TopoDS_Face face, double area, gp_Pln plane)>();
//             var faces = shape.Faces();
//             foreach (var face in faces)
//             {
//                 var brepAdaptor = new BRepAdaptor_Surface(face);
//                 if (brepAdaptor.Type != GeomAbs_SurfaceType.GeomAbs_Plane)
//                     continue;
// 
//                 var area = face.Area;
//                 var plane = brepAdaptor.Plane;
//                 if (face.Orientation == TopAbs_Orientation.TopAbs_REVERSED)
//                 {
//                     plane.Position.ZReverse();
//                 }
//                 faceInfos.Add((face, area, plane));
// 
//                 if (foundFace != null && foundFace.IsSame(face))
//                 {
//                     // Same edge with another orientation
//                     if (foundFace.Orientation == TopAbs_Orientation.TopAbs_REVERSED
//                         && face.Orientation == TopAbs_Orientation.TopAbs_FORWARD)
//                     {
//                         // Prefer forward edges
//                         foundFace = face;
//                     }
//                     continue;
//                 }
// 
//                 if (foundFace == null || comparer(area, faceArea))
//                 {
//                     foundFace = face;
//                     faceArea = area;
//                     foundPlane = plane;
//                 }
//             }
// 
//             if (foundPlane != null)
//             {
//                 // Find opposite edge
//                 faceArea = double.NaN;
//                 foreach (var fi in faceInfos)
//                 {
//                     if (ReferenceEquals(fi.face, foundFace))
//                         continue;
//                     if (!fi.plane.Axis.IsParallel(foundPlane.Axis, 0.00001))
//                         continue;
// 
//                     if (opFace == null || comparer(fi.area, faceArea))
//                     {
//                         opFace = fi.face;
//                         faceArea = fi.area;
//                         opPlane = fi.plane;
//                     }
//                 }
//             }
//             return (foundFace, foundPlane, opFace, opPlane);
//         }

        /// <summary>
        /// Finds the face nearest to plane.
        /// The start face is a face which is parallel to the base plane of the shape (X/Y plane).
        /// Take the face with the shortest distance to the plane. This should work for 99%.
        /// </summary>
        /// <param name="sourceShape">The source shape.</param>
        /// <param name="plane">The plane.</param>
        /// <returns>TopoDS_Face.</returns>
        public static TopoDS_Face FindFaceNearestToPlane(TopoDS_Shape sourceShape, gp_Pln plane)
        {
            if (sourceShape == null)
                return null;

            var faces = sourceShape.Faces();
            TopoDS_Face bestMatch = null;
            double bestDistance = double.MaxValue;

            foreach (var face in faces)
            {
                if (!GetPlaneFromFace(face, out var facePlane))
                    continue;

                if (!facePlane.Axis.IsParallel(plane.Axis, 0.01))
                    continue;

                var dist = facePlane.SquareDistance(plane);
                if (dist >= bestDistance)
                    continue;

                bestMatch = face;
                bestDistance = dist;
            }
            return bestMatch;
        }

        /// <summary>
        /// Enum FaceDistanceMethod
        /// </summary>
        public enum FaceDistanceMethod
        {
            /// <summary>
            /// The use normal center
            /// </summary>
            UseNormalCenter,
            /// <summary>
            /// The use face plane
            /// </summary>
            UseFacePlane
        }

        /// <summary>
        /// Finds the opposite face.
        /// </summary>
        /// <param name="sourceShape">The source shape.</param>
        /// <param name="refFace">The reference face.</param>
        /// <param name="farthest">if set to <c>true</c> [farthest].</param>
        /// <param name="distanceMethod">The distance method.</param>
        /// <returns>TopoDS_Face.</returns>
        /// <exception cref="ArgumentOutOfRangeException">distanceMethod - null</exception>
        public static TopoDS_Face FindOppositeFace(TopoDS_Shape sourceShape, TopoDS_Face refFace, bool farthest = false,
                                                   FaceDistanceMethod distanceMethod = FaceDistanceMethod.UseNormalCenter)
        {
            if (sourceShape == null || refFace == null)
                return null;

            gp_Pln refPlane = new gp_Pln();
            if (distanceMethod == FaceDistanceMethod.UseFacePlane)
            {
                if (!GetPlaneFromFace(refFace, out refPlane))
                    return null;
            }

            var refNormal = GetFaceCenterNormal(refFace);

            // Iterate faces
            var faces = sourceShape.Faces();
            TopoDS_Face bestMatch = null;
            double bestDistance = farthest ? double.MinValue : double.MaxValue;

            foreach (var face in faces)
            {
                if (face.IsEqual(refFace))
                    continue;

                var faceNormal = GetFaceCenterNormal(face);
                if (!faceNormal.IsOpposite(refNormal, 0.01))
                    continue;

                double dist = 0;
                switch (distanceMethod)
                {
                    case FaceDistanceMethod.UseNormalCenter:
                        dist = faceNormal.Location.SquareDistance(refNormal.Location);
                        break;
                    case FaceDistanceMethod.UseFacePlane:
                        if (!GetPlaneFromFace(face, out var facePlane))
                            continue;
                        dist = facePlane.Distance(refPlane);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException(nameof(distanceMethod), distanceMethod, null);
                }
                if (farthest ? dist <= bestDistance : dist >= bestDistance)
                    continue;

                bestMatch = face;
                bestDistance = dist;
            }

            return bestMatch;
        }

    }
}
